﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeGeneration;
using Microsoft.CodeAnalysis.CodeRefactorings;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Simplification;
using Microsoft.CodeAnalysis.Text;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Common.CSharp
{
    /// <example>public string Name { get; set; }</example>
    [ExportCodeRefactoringProvider("ExpandAutoProperty", LanguageNames.CSharp)]
    internal partial class ExpandAutoPropertyCodeRefactoringProvider : ICodeRefactoringProvider
    {
        public async Task<IEnumerable<CodeAction>> GetRefactoringsAsync(Document document, TextSpan span, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken).ConfigureAwait(false) as CompilationUnitSyntax;

            var property = root.FindToken(span.Start).Parent.AncestorsAndSelf().OfType<PropertyDeclarationSyntax>().FirstOrDefault();

            if (property == null || property.AccessorList == null)
                return null;

            if (property.AccessorList.Accessors.Any(a => a.IsKind(SyntaxKind.GetAccessorDeclaration) && a.Body == null) && property.AccessorList.Accessors.Any(a => a.IsKind(SyntaxKind.SetAccessorDeclaration) && a.Body == null))
                return new[] { CodeAction.Create("Expand property declaration", c => ExpandAutoPropertyAsync(root, document, property, c)) };

            return null;
        }

        protected virtual async Task<Document> ExpandAutoPropertyAsync(SyntaxNode root, Document document, PropertyDeclarationSyntax property, CancellationToken c)
        {
            SemanticModel model = await document.GetSemanticModelAsync(c).ConfigureAwait(false);

            // compute the name of the backing field
            string fieldName = GenerateBackingFieldName(property, model, c);

            // find declaring type
            TypeDeclarationSyntax type = property.Ancestors().OfType<TypeDeclarationSyntax>().First();

            // add backing field to type
            TypeDeclarationSyntax newType = type
                .WithBackingField(model.GetTypeInfo(property.Type).Type, fieldName, document.Project.Solution.Workspace);

            // find property declaration in new type
            property = newType.Members.OfType<PropertyDeclarationSyntax>().Single(m => property.IsEquivalentTo(m));

            AccessorDeclarationSyntax
                getter = property.AccessorList.Accessors.Single(a => a.IsKind(SyntaxKind.GetAccessorDeclaration)),
                setter = property.AccessorList.Accessors.Single(a => a.IsKind(SyntaxKind.SetAccessorDeclaration));

            // expand accessors
            var newNode = property
                .WithAccessorList(
                    SyntaxFactory.AccessorList(
                        new SyntaxList<AccessorDeclarationSyntax>()
                            .Add(
                                getter.WithBody(
                                    SyntaxFactory.Block(
                                        SyntaxFactory.ReturnStatement(
                                            SyntaxFactory.IdentifierName(fieldName)
                                        )
                                    )
                                )
                                    .WithSemicolonToken(default(SyntaxToken))
                                    .WithTrailingTrivia(default(SyntaxTriviaList))
                            )
                            .Add(
                                setter.WithBody(
                                    SyntaxFactory.Block(
                                        SyntaxFactory.ExpressionStatement(
                                            SyntaxFactory.BinaryExpression(SyntaxKind.SimpleAssignmentExpression,
                                                SyntaxFactory.MemberAccessExpression(SyntaxKind.SimpleMemberAccessExpression,
                                                    SyntaxFactory.ThisExpression(),
                                                    SyntaxFactory.IdentifierName(fieldName)
                                                )
                                                    .WithAdditionalAnnotations(Simplifier.Annotation),
                                                SyntaxFactory.IdentifierName("value")
                                            )
                                        )
                                    )
                                )
                                    .WithSemicolonToken(default(SyntaxToken))
                            )
                    )
                )
                .WithAdditionalAnnotations(Formatter.Annotation)
            ;

            // replace nodes
            newType = newType.ReplaceNode(property, newNode);

            var newRoot = root
                .ReplaceNode(type, newType)
            ;

            return document.WithSyntaxRoot(newRoot);
        }

        private static string GenerateBackingFieldName(PropertyDeclarationSyntax property, SemanticModel semanticModel, CancellationToken c)
        {
            // backing field name
            var baseName = property.Identifier.ValueText;
            baseName = string.Concat('_', char.ToLower(baseName[0]), baseName.Substring(1));

            var propertySymbol = semanticModel.GetDeclaredSymbol(property, c);
            if (propertySymbol == null ||
                propertySymbol.ContainingType == null)
                return baseName;

            // generate new name with index
            var index = 0;
            var name = baseName;

            while (propertySymbol.ContainingType.MemberNames.Contains(name))
                name = baseName + ++index;

            return name;
        }
    }

    public static partial class TypeExtensions
    {
        public static TypeDeclarationSyntax WithBackingField(this TypeDeclarationSyntax node, ITypeSymbol type, string name, Workspace workspace)
        {
            var newField = CodeGenerationSymbolFactory.CreateFieldSymbol(
                attributes: null,
                accessibility: Accessibility.Private,
                modifiers: new SymbolModifiers(),
                type: type,
                name: name
            );

            return CodeGenerator.AddFieldDeclaration(node, newField, workspace);
        }
    }
}
